package linkFinder.js;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;
import linkFinder.FindHTMLTag;

public class JSFunction {
	protected Map<Integer,FunctionParameter> parameters = new TreeMap<Integer,FunctionParameter>();
	protected String functionName = new String();
	protected Map<String,FunctionInstance> instances = new  TreeMap<String,FunctionInstance>();
	protected String functionBody = new String();
	protected Map<String, JSFunction> jsList = new TreeMap<String, JSFunction>();
	
	public static final char PLUS = '+';  
	public static final char MINUS = '-';
	public static final char MULTIPLY  = '*';
	public static final char DIVIDE = '/';
	public static final char MOD = '%';
	public static final char POWER = '^';
	public static final char OPEN = '(';
	public static final char CLOSE = ')';
	public static final char COMMAS = '"';
	public static final char APOSTROPHE = '\'';
	public static final char SPACE = ' ';
	public static final char EQUAL = '=';
	
	public static Map<Character,Integer> priority = new TreeMap<Character,Integer>();
	{
		priority.put(PLUS, 0);
		priority.put(MINUS, 0);
		priority.put(MULTIPLY, 1);
		priority.put(DIVIDE, 1);
		priority.put(MOD, 1);
		priority.put(POWER, 1);
		priority.put(OPEN, 3);
		priority.put(CLOSE, 3);
	}
	
	public static Map<String,JSInternalFunction> JS = new TreeMap<String,JSInternalFunction>();
	{
		//here will be placed values of javascript internal
		//functions and expressions
	}
	public static final int MATH_EXPR = 0;
	public static final int STRING_EXPR = 1;
	public static final int DATE_EXPR = 2;
	
	public JSFunction(String func){
		extractFunctionName(func);
		extractFunctionBody(func);
		extractFunctionParameters(func);
		extractFunctionInstances(func);
	}
	
	public int getPrametersSize(){
		return this.parameters.size();
	}
	
	public void setFunctionList(Map<String, JSFunction> list){
		this.jsList = new TreeMap<String, JSFunction>(list);
	}
	
	public Collection<FunctionParameter> getFunctionParameterList(){
		return parameters.values();
	}
	
	public void setParameterValue(Integer order, String value){
		if(this.parameters.containsKey(order)){
			value = value.trim();
			if(value.startsWith("\"") && value.endsWith("\"")){
				value = value.substring(1,value.length()-1); 
			}else if(value.startsWith("'") && value.endsWith("'")){
				value = value.substring(1,value.length()-1);
			}
			FunctionParameter fp = this.parameters.remove(order);
			fp.setVariableValue(value);
			this.parameters.put(order, fp);
		}
	}
	
	public FunctionParameter getFunctionParameter(Integer order){
		return (this.parameters.containsKey(order) ? this.parameters.get(order) : null);
	}
	
	public boolean isExistFunctionParameter(Integer order){
		return this.parameters.containsKey(order);
	}
	
	/*
	 * 
	 * @param func
	 */
	protected void extractFunctionName(String func){
		String wholeFunc = func.trim();
		int beginIndex = wholeFunc.indexOf("function ") + "function ".length();
		int endIndex = wholeFunc.indexOf("(");
		if(endIndex > beginIndex){
			this.setFunctionName( wholeFunc.substring(beginIndex, endIndex).trim() );
		}else{
			System.out.println(wholeFunc);
		}
	}
	
	/*
	 * 
	 * @param func
	 */
	protected void extractFunctionBody(String func){
		int beginIndex = func.indexOf("{");
		int endIndex = func.lastIndexOf("}");
		if(endIndex - beginIndex > 0 && endIndex-1 < func.length()){
			String body = func.substring(beginIndex+1, endIndex-1);
			//removes comments
			StringBuffer work_text = new StringBuffer ();		
			StringTokenizer tokens = new StringTokenizer(body,"\n");
			while(tokens.hasMoreTokens()){
				String token = tokens.nextToken();
				if(!token.startsWith("//")){
					work_text.append(token+"\n");
				}
			}
	
			this.setFunctionBody( work_text.toString() );
		}
	}
	
	/*
	 * 
	 * @param func
	 */
	protected void extractFunctionParameters(String func){
		if("".equals(this.functionName)){
			extractFunctionName(func);
		}
		int beginIndex = func.indexOf("(");
		int endIndex = func.indexOf(")");
		if(endIndex - beginIndex > 2){
			String paramList = func.substring(beginIndex+1, endIndex);
			StringTokenizer tokens = new StringTokenizer(paramList,",");
			int ind = 0;
			while(tokens.hasMoreTokens()){
				String param = tokens.nextToken().trim();
				if(param != null && !"".equals(param)){
					FunctionParameter fp = new FunctionParameter(ind, param, this.getFunctionName());
					this.parameters.put(ind, fp);
					ind++;
				}
			}
		}
	}
	
	/*
	 * extracts function instances 
	 */
	protected void extractFunctionInstances(String func){
		if(!FindHTMLTag.isNullEmptyString(func) && func.indexOf("var") != -1){
			StringTokenizer tokens = new StringTokenizer(func,"\n");
			while(tokens.hasMoreTokens()){
				String line = tokens.nextToken();
				StringTokenizer elems = new StringTokenizer(line,";");
				while(elems.hasMoreTokens()){
					String varLine = elems.nextToken();
					if(varLine.indexOf("=") != -1 && varLine.trim().startsWith("var")){
						int ind = varLine.indexOf("=");
						String left = varLine.substring(varLine.indexOf("var")+"var".length(), ind).trim();
						String right = varLine.substring(ind+1).trim();
						String val = null;
						if(!FindHTMLTag.isNullEmptyString(left) 
								&& !FindHTMLTag.isNullEmptyString(right) 
								&& ( (right.startsWith("\"") && right.startsWith("\"") ) 
										|| (right.startsWith("'") && right.startsWith("'") ))
						){
							FunctionInstance fi = new FunctionInstance(left,this.functionName);
							fi.setVariableValue(val);
							instances.put(left, fi);
						}
					}
				}
			}
		}
	}
	
	/*
	 * 
	 * @return
	 */
	protected Map<String, FunctionParameter> getParametersByName(){
		Map<String, FunctionParameter> mp = new TreeMap<String, FunctionParameter>();
		Iterator<FunctionParameter> i = this.parameters.values().iterator();
		while(i.hasNext()){
			FunctionParameter fp = i.next();
			mp.put(fp.getVariableName(), fp);
		}
		return mp;
	}
	
	/*
	 * 
	 * @param line
	 * @return
	 */
	protected String getEvalLine(String line){
		int ind = line.indexOf("eval(");
		if(ind != -1){
			String code = line.substring(ind+"eval(".length());
			int counter = 1;
			int text_ind = 0;
			while(counter != 0 && text_ind < code.length()){
				if(code.charAt(text_ind) == JSFunction.OPEN){
					counter++;
				}else if(code.charAt(text_ind) == JSFunction.CLOSE){
					counter--;
				}
				text_ind++;
			}
			return code.substring(0, text_ind-1);
		}
		return new String();
	}
	
	/*
	 * checking if character is alphanumeric
	 * @param ch character to be checked
	 * @return <tt>true</tt> if is alphanumeric 
	 */
	protected boolean isLetter(char ch){
		return (ch > 'a' && ch < 'z') || (ch > 'A' && ch < 'Z') || (ch > '0' && ch <'9');
	}
	
	protected String replaceParamByValue(String expression){
		Iterator<FunctionParameter> intr = this.parameters.values().iterator();
		while(intr.hasNext()){
			FunctionParameter fnp = intr.next();
			String paramName = fnp.getVariableName();
			String paramValue = String.valueOf(fnp.getVariableValue());
			int prPlace = -1;
			int lastPlace = 0;
			StringBuffer snb = new StringBuffer();
			while((prPlace = expression.indexOf(paramName,lastPlace)) != -1){
				if(prPlace > 0 
						&& prPlace < expression.length()-1 
						&& !isLetter(expression.charAt(prPlace-1)) 
						&& !isLetter(expression.charAt(prPlace+paramName.length()))){
					snb.append(expression.substring(lastPlace, prPlace) + "\"" + paramValue + "\"");
					lastPlace = prPlace+paramName.length();
				}
			}
			if(snb.length() <= 0){
				snb.append(expression);
			}else{
				snb.append(expression.substring(lastPlace));
			}
			return snb.toString();
		}
		
		return expression;
	}
	
	protected String replaceInstanceByValue(String expression){
		Iterator<FunctionInstance> intr = this.instances.values().iterator();
		while(intr.hasNext()){
			FunctionInstance fnp = intr.next();
			String paramName = fnp.getVariableName();
			String paramValue = String.valueOf(fnp.getVariableValue());
			int prPlace = -1;
			int lastPlace = 0;
			StringBuffer snb = new StringBuffer();
			while((prPlace = expression.indexOf(paramName,lastPlace)) != -1){
				if(prPlace > 0 
						&& prPlace < expression.length()-1 
						&& !isLetter(expression.charAt(prPlace-1)) 
						&& !isLetter(expression.charAt(prPlace+paramName.length()))){
					snb.append(expression.substring(lastPlace, prPlace) + "\"" + paramValue + "\"");
					lastPlace = prPlace+paramName.length();
				}
			}
			if(snb.length() <= 0){
				snb.append(expression);
			}else{
				snb.append(expression.substring(lastPlace));
			}
			return snb.toString();
		}
		
		return expression;
	}
	
	/*
	 * 
	 * @param expression
	 * @param lst
	 * @return
	 */
	protected String extractEval(String expression, LinkedList<String> lst){
		StringBuffer sb = new StringBuffer();
		if(!FindHTMLTag.isNullEmptyString(expression)){
			Map<String, FunctionParameter> fps = this.getParametersByName();
			StringTokenizer line = new StringTokenizer(expression,"\n");
			while(line.hasMoreTokens()){
				StringTokenizer codeLine = new StringTokenizer(line.nextToken(),";");
				while(codeLine.hasMoreTokens()){
					String code = codeLine.nextToken().trim();
					int start_eval = code.indexOf("eval");
					if(start_eval != -1){
						code = getEvalLine(code);
						StringTokenizer tk = new StringTokenizer(code,String.valueOf(JSFunction.PLUS));
						StringBuffer ln = new StringBuffer();
						while(tk.hasMoreTokens()){
							String tmp = tk.nextToken().trim();
							if(tmp.startsWith("\"") && tmp.endsWith("\"")){
								//if evals starts with upper commas
								ln.append(tmp.substring(tmp.indexOf("\"")+1, tmp.lastIndexOf("\""))); 
							}else if(tmp.startsWith("'") && tmp.endsWith("'")){
								//if evals starts with apostrophe
								ln.append(tmp.substring(tmp.indexOf("'")+1, tmp.lastIndexOf("'")));
							}else if(fps.containsKey(tmp.toLowerCase())){
								//if needs to change parameter name by its value (most of params changed before)
								FunctionParameter fp = fps.get(tmp.toLowerCase());
								String pValue = String.valueOf(fp.getVariableValue());
								if(!FindHTMLTag.isNullEmptyString(pValue)){
									ln.append(pValue);
								}
							}else if(instances.containsKey(tmp.trim())){
								FunctionInstance inst = instances.get(tmp.trim());
								String pValue = (String)inst.getVariableValue();
								if(pValue != null){
									ln.append(pValue);
								}
							}else{
								if(tmp.indexOf(JSFunction.OPEN) != -1 && tmp.indexOf(JSFunction.CLOSE) != -1){
									String jsName = tmp.substring(0,tmp.indexOf(JSFunction.OPEN)).trim();
									String parameters = tmp.substring(tmp.indexOf(JSFunction.OPEN)+1,tmp.lastIndexOf(JSFunction.CLOSE)).trim();
									if(jsList.containsKey(jsName)){
										JSFunction jsf = jsList.get(jsName);
										if(jsf != null){
											int i=0;
											StringTokenizer toks = new StringTokenizer(parameters,",");
											while(toks.hasMoreTokens()){
												jsf.setParameterValue(i, toks.nextToken());
												i++;
											}
											LinkedList<String> lt = jsf.parseExpression(jsf.getFunctionBody(), lst);
											if(!lt.isEmpty()){
												lst.addAll(lt);
											}
										}
									}
								}
							}
						}
						if(!FindHTMLTag.isNullEmptyString(ln.toString())){
							sb.append(ln.toString() + "\n"); 
						}
					}else{
						sb.append(code + "\n");
					}
				}
			}
		}
		return sb.toString();
	}
	
	/*
	 * 
	 * @param code
	 * @param lst
	 * @param begin_from
	 */
	protected void extractLocationHref(String code, LinkedList<String> lst, int begin_from){
		code = code.substring(begin_from+"location.href".length()).trim();
		int eq_ind = code.indexOf(JSFunction.EQUAL);
		if(eq_ind != -1){
			code = code.substring(eq_ind+1).trim();
			code = removeStringSign(code).trim();
			lst.add(code);
		}
	}
	
	/*
	 * 
	 * @param code
	 * @param lst
	 * @param begin_from
	 * @param end
	 */
	protected void extractIframe(String code, LinkedList<String> lst, int begin_from,int end){
		code = code.substring(begin_from + "<iframe".length(), end);
		int eq_src = code.indexOf("src=");
		if(eq_src != -1){
			code = code.substring(eq_src+"src=".length()).trim();
			Character begin = null;
			if(code.startsWith("\"")){
				begin = '"';
			}else if(code.startsWith("'")){
				begin = '\'';
			}
			if(begin != null){
				int i=1;
				while(code.charAt(i) != begin && i<code.length()){
					i++;
				}
				code = code.substring(1,i);
				code = removeStringSign(code).trim();
				lst.add(code);
			}
			
		}
	}
	
	/**
	 * recursive function for semi-parsing the js expression
	 * @param arg0 
	 * @param lst
	 * @return
	 */
	public LinkedList<String> parseExpression(String arg0, LinkedList<String> lst){
		String expression = replaceParamByValue(arg0);
		expression = replaceInstanceByValue(expression);
		expression = extractEval(expression, lst);
		StringTokenizer line = new StringTokenizer(expression,"\n");
		while(line.hasMoreTokens()){
			StringTokenizer codeLine = new StringTokenizer(line.nextToken(),";");
			while(codeLine.hasMoreTokens()){
				String code = codeLine.nextToken().trim();
				int loc_ind = -1;
				int end_if = -1;
				if((loc_ind = code.indexOf("location.href"))!=-1){
					extractLocationHref(code,lst,loc_ind);
				}else if((loc_ind = code.toLowerCase().indexOf("<iframe"))!=-1 && (end_if = code.toLowerCase().indexOf("</iframe>"))!=-1){
					extractIframe(code,lst,loc_ind,end_if);
				}
			}
		}
		return lst;
	}
	
	/*
	 * removes string signs 
	 * @param value
	 * @return
	 */
	protected String removeStringSign(String value){
		StringBuffer sb = new StringBuffer();
		for(int i=0; i<value.length(); i++){
			char ch = value.charAt(i); 
			if(ch != JSFunction.COMMAS && ch != JSFunction.APOSTROPHE){
				sb.append(ch);
			}
		}
		return sb.toString();
	}
	
	public LinkedList<String> parseFunction(){
		LinkedList<String> val =  parseExpression(this.functionBody, new LinkedList<String>());
		
		return ( val != null && !val.isEmpty() ?  val : null);
	}
	
	protected void setFunctionName(String name){
		this.functionName = name;
	}
	
	public String getFunctionName(){
		return this.functionName;
	}
	
	protected void setFunctionBody(String body){
		this.functionBody = body;
	}
	
	public String getFunctionBody(){
		return this.functionBody;
	}
}
